Skip to content

fix: working runner token exchange + OSC_REFRESH_TOKEN (stage 1 of #39) - #41

Merged
eyevinn-developer[bot] merged 1 commit into
mainfrom
fix-runner-token-refresh-stage1
Nov 8, 2026
Merged

eyevinn-developer[bot] merged 1 commit into
mainfrom
fix-runner-token-refresh-stage1

Conversation

@eyevinn-developer

Copy link
Copy Markdown
Contributor

Part of #39

Stage 1 of the decision in #39 (comment). Stage 2 is NOT in this PR and #39 stays open for it: making the app-visible OSC_ACCESS_TOKEN the 1h PAT ships together with the REJECT_RUNNER_REFRESH_TOKENS rollout.

What changed

  • scripts/docker-entrypoint.sh: the refresh exchange now POSTs {"token": "<jwt>"} (JSON body from a mode-600 temp file, Content-Type: application/json, --max-time 10) to /runner-token/refresh, as web-runner does. config-to-env is run with the fresh 1h PAT set inline on that command only.
  • The shell's OSC_ACCESS_TOKEN is never assigned, so the app still receives the unchanged 365-day token. The app additionally gets OSC_REFRESH_TOKEN (the original token, unless already set).
  • The response is parsed with node (no jq in the image); an empty parse result counts as an exchange failure.
  • || true is replaced by a [CONFIG] WARNING: runner token exchange failed (HTTP <status>, code=<code>); using the original token line (status and code only, no body, no token). No new failure mode: the config load stays the only fatal path.
  • OSC_REFRESH_TOKEN and the fresh token are added to redact_config_log.
  • README documents OSC_REFRESH_TOKEN with a Python snippet.

Tests

New tests/test-entrypoint-runner-token-refresh.sh (9 cases, with a jq stub that exits 127): success asserts npx saw the fresh token, the app-visible OSC_ACCESS_TOKEN is unchanged and OSC_REFRESH_TOKEN is set; failures (401 expired, 401 invalid, 500, curl exit 22 and 28, 200 without token, 200 non-JSON) assert warning, original token used, continues, no token in stderr. All tests/*.sh run locally and pass (no PR CI runs tests in this repo).

Changes for existing consumers

Nothing observable: the app's OSC_ACCESS_TOKEN is byte-identical to today (the assignment is inline on the config-to-env command, never in the shell); OSC_REFRESH_TOKEN is a new additive variable (a repo-wide search by the architect found no other use; a parameter store key of that name overrides it).

What a human does next: a runner image rebuild/release is birme's call; nothing else for this stage.

Lessons

The entrypoint's shell assignment OSC_ACCESS_TOKEN=... keeps the export attribute, so assigning the exchange result would silently have shipped Stage 2; setting it inline on the config-to-env command avoids that. The existing harness curl stub exits 22, so every pre-existing test already runs through the new failure path.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@eyevinn-pr-reviewer eyevinn-pr-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Research Review

Verified

  • Request shape: POST /runner-token/refresh takes JSON { token } and returns { token, expiresIn }. I confirmed this in osaas-token-service src/model.ts (RunnerRefreshRequest and RunnerRefreshResponse). The route is registered in src/api_service_token.ts:549. The PR's body, header and .token parsing match.
  • Reference implementation: web-runner scripts/docker-entrypoint.sh (main) sends the same Content-Type: application/json request with --data @file from a mode-600 temp file, so the PR follows the existing pattern.
  • jq absence: the PR parses with node, as the architect note on #39 directed. The test stubs jq with exit 127 and the success case still passes. grep jq Dockerfile finds nothing.
  • Stage split matches birme's decision on #39 (comment 6719183061). The shell OSC_ACCESS_TOKEN is never assigned; the fresh token is set inline on the config-to-env command only. I checked lines 236-278 of the PR head: the export attribute never reaches the app, and OSC_REFRESH_TOKEN is exported.
  • Tests: I ran every tests/*.sh on the PR head and all pass, including the new one (9/9) and the existing config-load-failure suite (11/11). The repo has no PR CI that runs tests, so gh pr checks reports nothing.
  • set -e safety: every new command that can fail (curl, node) is guarded by || var=..., so no new fatal path is added. The config load remains the only fatal path.
  • Redaction: OSC_REFRESH_TOKEN and config_token were added to redact_config_log. The warning line prints only the status and a sanitised code.
  • ## Lessons is present and filled in.

Adjacent Stubs Detected

  • None. I grepped scripts/docker-entrypoint.sh for undefined : undefined, || undefined, TODO/FIXME/HACK and empty catch blocks and found no stubs in the touched code. The remaining || true uses are on unrelated cat calls.

Orphaned Files Detected

  • None. The entrypoint is the Dockerfile entry point. The test follows the tests/*.sh convention. README is documentation.

Escape Hatch Coverage

  • The diff adds no terminal-rejection branch. An exchange failure only warns and falls back to the original token. Passing-path and failure-path tests both exist (tests/test-entrypoint-runner-token-refresh.sh).

Object Shape Verification

  • No spreads of fetched objects. The only parse is JSON.parse on the real response shape, and it fails closed.

Contract diff

  • No request handler changed. Files checked: scripts/docker-entrypoint.sh, README.md, tests/test-entrypoint-runner-token-refresh.sh.
  • Container-level contract (the app environment): OSC_ACCESS_TOKEN stays the byte-identical 365-day token. OSC_REFRESH_TOKEN is a new optional variable, which is an additive change. A repo-wide search found no existing consumer of the name, and ADR-0005 uses the same name. Stage 2, which changes the app-visible token, is explicitly not in this PR. Birme's staged decision is on #39: #39 (comment)
  • Timeout: curl --max-time 10 on the startup path, not a request path. Measurement is cited on #39 (dev p50 36 ms, max 133 ms; prod p50 34 ms, max 38 ms). The timeout branch falls back to the original token and continues, so it never becomes a terminal failure. Worst-case added boot delay is 10 s, and only if token-service hangs.

Impact Analysis

  • Blast radius: python-runner entrypoint only. This runs for any app with OSC_ACCESS_TOKEN and CONFIG_SVC set.
  • Downstream consumers: apps read the unchanged OSC_ACCESS_TOKEN. A parameter-store key named OSC_REFRESH_TOKEN overrides the new variable, and the README documents this.
  • Coordinated changes needed: none. A runner image release is a human call, per the PR body. Sibling tickets (wasm, golang, dotnet, python-job-runner) are separate.
  • Breaking change risk: low.

Risks Not Tested

  • Live exchange against a real token-service with a genuine runner-refresh token was not run. The tests use a curl stub. The request shape was checked against the source and web-runner, not against a live 200.
  • Behaviour of @osaas/cli config-to-env with a 1 h PAT was not tested. Web-runner already relies on the same behaviour.
  • The built image was not inspected for node or jq. The check was based on the Dockerfile, so this is unverified at runtime.
  • Dev verification after merge (remake plus pod log) is still pending.

Research Suggestions

  • After release, verify the [CONFIG] warning does not appear in a dev pod log. This is the real-world confirmation of the exchange.

Verdict

No blocking issues found.


Reviewed by PR Research Validator. This review checks whether code was based on verified sources rather than assumptions.

@eyevinn-developer
eyevinn-developer Bot merged commit 4e01bf2 into main Nov 8, 2026
@eyevinn-developer
eyevinn-developer Bot deleted the fix-runner-token-refresh-stage1 branch November 8, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants